﻿namespace LivePatterns
{
	using System;
	using System.Collections.Generic;
	using System.ComponentModel;
	using System.Linq;
	using System.Reflection;
	using System.Reflection.Emit;

	public static partial class Build
	{
		/// <summary>
		/// Creates an instance that implements the specified interface.
		/// </summary>
		/// <typeparam name="TInterface"></typeparam>
		/// <returns>Returns an instance that implements a specified interface.</returns>
		/// <remarks>Every property and field is initialized to its default value. Void methods are empty inside, non-void methods return the default value of their return types.</remarks>
		/// <example>
		/// IArticle mock = Build.Implement&lt;IArticle&gt;();
		/// mock.Title = "Instance for testing";
		/// mock.Published = DateTime.Now;
		/// </example>
		public static TInterface Implement<TInterface>() where TInterface : class
		{
			if (!typeof(TInterface).IsInterface)
				throw new ArgumentException("Only interface types are supported.");

			// resolve type from cache, if already built
			if (_implementationTypeCache.ContainsKey(typeof(TInterface)))
				return Activator.CreateInstance(_implementationTypeCache[typeof(TInterface)]) as TInterface;

			TypeBuilder typeBuilder = moduleBuilder.DefineType(String.Format("{0}_Implementation_{1}", typeof(TInterface).Name, Guid.NewGuid().ToString().Replace("-", "")), TypeAttributes.Class | TypeAttributes.Public);
			typeBuilder.AddInterfaceImplementation(typeof(TInterface));

			// implement fields
			foreach (FieldInfo fi in typeof(TInterface).GetFields(BindingFlags.Public | BindingFlags.Instance).Where(m => !m.IsSpecialName))
			{
				FieldBuilder fb = typeBuilder.DefineField(fi.Name, fi.FieldType, FieldAttributes.Public);
			}

			// implement properties
			foreach (PropertyInfo pi in typeof(TInterface).GetAllVisibleProperties())
			{
				// define property
				PropertyBuilder pb = typeBuilder.DefineProperty(pi.Name, PropertyAttributes.None, typeof(int), Type.EmptyTypes);
				MethodBuilder methodBuilder;
				ILGenerator ilGenerator;

				// define backing field
				FieldBuilder fieldBuilder = typeBuilder.DefineField(String.Format("<{0}>k__BackingField{0}", pb.Name), pi.PropertyType, FieldAttributes.Private | FieldAttributes.SpecialName);

				//
				// implement get method
				//
				if (pi.CanRead)
				{
					MethodInfo getMethod = pi.GetGetMethod();
					methodBuilder = typeBuilder.DefineMethod(
						getMethod.Name, _methodAttributes, getMethod.ReturnType, getMethod.GetParameters().Select(p => p.ParameterType).ToArray()
					);

					ilGenerator = methodBuilder.GetILGenerator();
					ilGenerator.Emit(OpCodes.Ldarg_0);
					ilGenerator.Emit(OpCodes.Ldfld, fieldBuilder);
					ilGenerator.Return();
					pb.SetGetMethod(methodBuilder);
				}

				//
				// implement set method
				//
				if (pi.CanWrite)
				{
					MethodInfo setMethod = pi.GetSetMethod();
					methodBuilder = typeBuilder.DefineMethod(
						setMethod.Name, _methodAttributes, setMethod.ReturnType, setMethod.GetParameters().Select(p => p.ParameterType).ToArray()
					);
					methodBuilder.DefineParameter(1, ParameterAttributes.None, "value");

					ilGenerator = methodBuilder.GetILGenerator();
					ilGenerator.Emit(OpCodes.Ldarg_0);
					ilGenerator.Emit(OpCodes.Ldarg_1);
					ilGenerator.Emit(OpCodes.Stfld, fieldBuilder);
					ilGenerator.Return();
					pb.SetSetMethod(methodBuilder);
				}
			}

			// implement methods
			foreach (MethodInfo mi in typeof(TInterface).GetAllVisibleMethods())
			{
				MethodBuilder methodBuilder = typeBuilder.DefineMethod(
					mi.Name, MethodAttributes.Public | MethodAttributes.Virtual, mi.ReturnType, mi.GetParameters().Select(p => p.ParameterType).ToArray()
				);

				ILGenerator ilGenerator = methodBuilder.GetILGenerator();

				if (mi.ReturnType != typeof(void))
				{
					ilGenerator.DeclareLocal(mi.ReturnType);
					ilGenerator.Emit(OpCodes.Ldloc_0);
				}

				ilGenerator.Return();
			}

			// implement events
			foreach (EventInfo ei in typeof(TInterface).GetAllVisibleEvents())
			{
				// define event
				EventBuilder eb = typeBuilder.DefineEvent(ei.Name, EventAttributes.None, ei.EventHandlerType);
				MethodBuilder methodBuilder;
				ILGenerator ilGenerator;

				// define backing field
				FieldBuilder fb = typeBuilder.DefineField(String.Format("{0}", ei.Name), ei.EventHandlerType, FieldAttributes.Private);

				//
				// implement add method
				//
				MethodInfo addMethod = ei.GetAddMethod();
				methodBuilder = typeBuilder.DefineMethod(
					addMethod.Name, _methodAttributes, addMethod.ReturnType, addMethod.GetParameters().Select(p => p.ParameterType).ToArray()
				);

				ilGenerator = methodBuilder.GetILGenerator();
				ilGenerator.Emit(OpCodes.Ldarg_0);
				ilGenerator.Emit(OpCodes.Ldarg_0);
				ilGenerator.Emit(OpCodes.Ldfld, fb);
				ilGenerator.Emit(OpCodes.Ldarg_1);
				ilGenerator.Emit(OpCodes.Call, typeof(Delegate).GetMethod("Combine", new Type[] { typeof(Delegate), typeof(Delegate) }));
				ilGenerator.Emit(OpCodes.Castclass, typeof(PropertyChangedEventHandler));
				ilGenerator.Emit(OpCodes.Stfld, fb);
				ilGenerator.Return();
				eb.SetAddOnMethod(methodBuilder);

				//
				// implement remove method
				//
				MethodInfo removeMethod = ei.GetRemoveMethod();
				methodBuilder = typeBuilder.DefineMethod(
					removeMethod.Name, _methodAttributes, removeMethod.ReturnType, removeMethod.GetParameters().Select(p => p.ParameterType).ToArray()
				);

				ilGenerator = methodBuilder.GetILGenerator();
				ilGenerator.Emit(OpCodes.Ldarg_0);
				ilGenerator.Emit(OpCodes.Ldarg_0);
				ilGenerator.Emit(OpCodes.Ldfld, fb);
				ilGenerator.Emit(OpCodes.Ldarg_1);
				ilGenerator.Emit(OpCodes.Call, typeof(Delegate).GetMethod("Remove", new Type[] { typeof(Delegate), typeof(Delegate) }));
				ilGenerator.Emit(OpCodes.Castclass, typeof(PropertyChangedEventHandler));
				ilGenerator.Emit(OpCodes.Stfld, fb);
				ilGenerator.Return();
				eb.SetRemoveOnMethod(methodBuilder);
			}

			Type resultType = typeBuilder.CreateType();
			_implementationTypeCache.Add(typeof(TInterface), resultType);
			return Activator.CreateInstance(resultType) as TInterface;
		}

		private static readonly IDictionary<Type, Type> _implementationTypeCache = new Dictionary<Type, Type>();
	}
}